/*
 * Copyright (C) 2008
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.googlecode.sca4idea.annotators;

import com.googlecode.sca4idea.SCABundle;
import com.googlecode.sca4idea.facet.SCAFacet;
import com.googlecode.sca4idea.providers.SCAProvider;
import com.googlecode.sca4idea.psi.PromotedWireable;
import com.googlecode.sca4idea.psi.SCAComponentTagPsiElement;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.lang.annotation.AnnotationHolder;
import com.intellij.lang.annotation.Annotator;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.XmlRecursiveElementVisitor;
import com.intellij.psi.xml.*;
import com.intellij.util.IncorrectOperationException;
import org.jetbrains.annotations.NotNull;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Annotator for detecting various errors that can occur within composites.
 */
public class CompositeAnnotator implements Annotator {
    private AnnotationHolder annotationHolder;
    private SCAProvider[] providers;

    public void annotate(PsiElement psiElement, AnnotationHolder annotationHolder) {
        this.annotationHolder = annotationHolder;

        if (psiElement instanceof XmlFile) {
            this.providers = SCAFacet.getProvidersForElement(psiElement);
            if (this.providers.length > 0) {
                psiElement.accept(new CompositeInspectionAnnotator());
            }
        }
    }

    private SCAComponentTagPsiElement getPSIElementForTag(XmlTag tag) {
        for (SCAProvider provider : providers) {
            SCAComponentTagPsiElement el = provider.getPSIElementForTag(tag);
            if (el != null) return el;
        }

        return null;
    }

    private class CompositeInspectionAnnotator extends XmlRecursiveElementVisitor {
        private boolean isComposite = false;
        private boolean isAutowiring = true;
        private Map<String, List<String>> promotedReferences = new HashMap<String, List<String>>();
        private Map<String, List<String>> promotedProperties = new HashMap<String, List<String>>();

        @Override
        public void visitXmlTag(XmlTag tag) {
            if (tag.getLocalName().equals("component") && isComposite) {
                visitComponentTag(tag);
            } else if (tag.getLocalName().equals("composite")) {
                visitCompositeTag(tag);

                isComposite = true;
            }

            super.visitXmlTag(tag);
        }

        private void visitCompositeTag(XmlTag tag) {
            validateChildNames(tag, "component", SCABundle.message("annotator.entity.component"), null);
            validateChildNames(tag, "service", SCABundle.message("annotator.entity.promoted.service"), null);
            Map<String, XmlTag> promotedRefs = validateChildNames(
                    tag, "reference", SCABundle.message("annotator.entity.promoted.reference"), null);
            validateChildNames(tag, "property", SCABundle.message("annotator.entity.promoted.property"), null);

            processPromotions(promotedRefs, promotedReferences, true);

            XmlAttribute autowireAttr = tag.getAttribute("autowire");
            if (autowireAttr != null && autowireAttr.getValue().equalsIgnoreCase("false")) {
                isAutowiring = false;
            }
        }

        private void visitComponentTag(XmlTag tag) {
            Map<String, XmlTag> references = validateChildNames(
                tag, "reference", SCABundle.message("annotator.entity.reference.wire"), promotedReferences);
            Map<String, XmlTag> properties = validateChildNames(
                    tag, "property", SCABundle.message("annotator.entity.property"), promotedProperties);
            String name = tag.getAttributeValue("name");

            SCAComponentTagPsiElement component = getPSIElementForTag(tag);
            if (component == null) {
                annotationHolder.createWarningAnnotation(getNameToken(tag),
                        SCABundle.message("annotator.warning.unrecognised.component.implementation.type"));
            } else {
                // Validate that all properties have been filled
                PromotedWireable[] allProps = component.getVariantsOfProperties(false);
                findMissingProperties(allProps, properties, name, tag);
                            
                if (!isAutowiring) {
                    // Since we aren't autowiring, we can validate that all references have been filled
                    PromotedWireable[] allRefs = component.getVariantsOfReferences(false);
                    findMissingReferences(allRefs, references, name, tag);
                }
            }
        }

        private Map<String, XmlTag> validateChildNames(XmlTag tag, String childName, String entityName,
                                                       Map<String, List<String>> promoted) {
            PsiElement[] children = tag.getChildren();
            Map<String, XmlTag> seenNames = new HashMap<String, XmlTag>();
            String containerTagName = tag.getAttributeValue("name");

            for (PsiElement child : children) {
                if (child instanceof XmlTag) {
                    XmlTag tagChild = (XmlTag) child;
                    if (tagChild.getLocalName().equals(childName)) {
                        String name = tagChild.getAttributeValue("name");
                        if (name == null) {
                            annotationHolder.createErrorAnnotation(
                                    tag, SCABundle.message("annotator.missing.name.attribute"));
                        } else {
                            if (seenNames.containsKey(name)) {
                                annotationHolder.createErrorAnnotation(tagChild,
                                        SCABundle.message("annotator.duplicate.definition", entityName, name)).
                                        registerFix(new DeleteTagIntentionAction(tagChild));
                                annotationHolder.createErrorAnnotation(seenNames.get(name),
                                        SCABundle.message("annotator.duplicate.definition", entityName, name)).
                                        registerFix(new DeleteTagIntentionAction(seenNames.get(name)));
                            } else if (promoted != null && containerTagName != null &&
                                    promoted.containsKey(containerTagName) &&
                                    promoted.get(containerTagName).contains(name)) {
                                annotationHolder.createErrorAnnotation(tagChild,
                                        SCABundle.message("annotator.duplicate.with.promotion", entityName, name)).
                                        registerFix(new DeleteTagIntentionAction(tagChild));
                            } else {
                                seenNames.put(name, tagChild);
                            }
                        }
                    }
                }
            }

            return seenNames;
        }

        /**
         * Indexes a series of promotion tags into the given index.
         * @param promotingTags the tags performing the promotion.
         * @param index the index.
         * @param requirePromoteAttr whether a promote attribute is required on the element.
         */
        private void processPromotions(Map<String, XmlTag> promotingTags, Map<String, List<String>> index,
                                       boolean requirePromoteAttr) {
            for (XmlTag tag : promotingTags.values()) {
                XmlAttribute promoteAttr = tag.getAttribute("promote");
                if (promoteAttr != null) {
                    String promotes = promoteAttr.getValue();
                    String[] promoted = promotes.split("\\s");
                    for (String promotedName : promoted) {
                        String[] parts = promotedName.split("/", 2);
                        if (parts.length == 2) {
                            // We can ignore anything that doesn't have the right size, since other validations will
                            // catch that.
                            if (!index.containsKey(parts[0])) {
                                index.put(parts[0], new ArrayList<String>());
                            }
                            index.get(parts[0]).add(parts[1]);
                        }
                    }
                } else if (requirePromoteAttr) {
                    annotationHolder.createErrorAnnotation(tag, SCABundle.message("annotator.error.no.promote.attribute"));
                }
            }
        }

        private void findMissingReferences(PromotedWireable[] allRefs, Map<String, XmlTag> references,
                                           String componentName, XmlTag errorTarget) {
            for (PromotedWireable ref : allRefs) {
                if (!references.containsKey(ref.getName()) && !ref.isOptional()) {
                    if (componentName == null || !promotedReferences.containsKey(componentName) ||
                         !promotedReferences.get(componentName).contains(ref.getName())) {
                        annotationHolder.createErrorAnnotation(getNameToken(errorTarget),
                                SCABundle.message("annotator.error.reference.is.not.being.wired", ref.getName()));
                    }
                }
            }
        }

        private void findMissingProperties(PromotedWireable[] allProps, Map<String, XmlTag> properties,
                                           String componentName, XmlTag errorTarget) {
            for (PromotedWireable prop : allProps) {
                if (!properties.containsKey(prop.getName()) && !prop.isOptional()) {
                    if (componentName == null || !promotedProperties.containsKey(componentName) ||
                         !promotedProperties.get(componentName).contains(prop.getName())) {
                        annotationHolder.createErrorAnnotation(getNameToken(errorTarget),
                                SCABundle.message("annotator.error.property.is.not.being.wired", prop.getName()));
                    }
                }
            }
        }

        private XmlToken getNameToken(XmlTag tag) {
            for (PsiElement child : tag.getChildren()) {
                if (child instanceof XmlToken && ((XmlToken) child).getTokenType().equals(XmlElementType.XML_NAME)) {
                    return (XmlToken) child;
                }
            }

            return null;
        }
    }

    private class DeleteTagIntentionAction implements IntentionAction {
        private final XmlTag tagChild;

        private DeleteTagIntentionAction(XmlTag tagChild) {
            this.tagChild = tagChild;
        }

        @NotNull
        public String getText() {
            return SCABundle.message("annotator.fix.remove.duplicate");
        }

        @NotNull
        public String getFamilyName() {
            return "sca.duplicates";
        }

        public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile psiFile) {
            return true;
        }

        public void invoke(@NotNull Project project, Editor editor, PsiFile psiFile) throws IncorrectOperationException {
            tagChild.delete();
        }

        public boolean startInWriteAction() {
            return true;
        }
    }
}
